﻿using System;
using System.Collections.Generic;
using Sustainalytics.Admin.Synchronization.Utility;
using Sustainalytics.DataLayer;
using Sustainalytics.DynamicsCRMGateway;
using Sustainalytics.Entities;
using System.Linq;
using Sustainalytics.AuthOperations;
using Sustainalytics.Utils;
using Sustainalytics.Admin.CRMSynchronization.Utility;
using Sustainalytics.Admin.CRMSynchronization.Exceptions;
using System.Text;

namespace Sustainalytics.Admin.CRMSynchronization
{
    
    /// asumptions:
    /// the accounts syncronization is single threaded an is always before the usersyncronization
    /// (the ReactivatedSyncEvents are consumed only by a usersyncronization thread)
    public class UsersSynchronization : IUsersSync
    {
        private const string _nullEmailListPlaceholder = "NULL_EMAIL_LIST";
        private const string _duplicateEmailListPlaceholder = "DUPLICATE_EMAIL_LIST";
        private const string _invalidUsersEmailSubjectFileName = "InvalidUsersEmailSubject.txt";
        private const string _invalidUsersEmailBodyFileName = "InvalidUsersEmailBody.htm";

        private IDynamicsCrmGateway _crmGateway;
        private IAccountsRepository _accountsRepository;
        private DateTime _currentSyncDate;
        private DateTime _lastSyncDate;
        private IAuthOperations _authOperation;
        private readonly Dictionary<Guid, Account> _cachedAccounts = new Dictionary<Guid, Account>();
        private readonly List<CrmContact> _crmNullEmailContacts = new List<CrmContact>();
        private readonly List<CrmContact> _crmDuplicatedEmailContacts = new List<CrmContact>();
        private List<Guid> _createdUsers = new List<Guid>();
        private List<Guid> _updatedUsers = new List<Guid>();
        private List<Guid> _invalidDuplicatedUsersIds = new List<Guid>();
        private List<Guid> _invalidNullEmailUsersIds = new List<Guid>();

        private UsersSynchronization() {
        }// prohibit public instantiation

        public static IUsersSync GetInstance(IDynamicsCrmGateway crmGateway, IAccountsRepository accountsRepository, IAuthOperations authOperations)
        {
            Guard.ArgumentNotNull(crmGateway, "crmGateway");
            Guard.ArgumentNotNull(accountsRepository, "accountsRepository");
            Guard.ArgumentNotNull(authOperations, "accountsRepository");

            return new UsersSynchronization
            {
                _crmGateway = crmGateway, 
                _accountsRepository = accountsRepository , 
                _authOperation = authOperations, 
            };
        }

        public void SyncUsers()
        {
            this._currentSyncDate = DateTime.UtcNow + this._crmGateway.GetCrmTimeOffset();
            CrmSyncEventSource.Log.UsersSyncStart(this._currentSyncDate);
            
            try
            {
                this._lastSyncDate = this._accountsRepository.GetLastSyncDate(SyncType.User);
              
                var deleted = SyncDeletedContacts();
                var result = new SyncResult { Deleted = deleted };
                CrmSyncEventSource.Log.UserSync(this._lastSyncDate,  "SyncDeletedContacts", result);

                var opResult = RecoverDirtyUsers();
                result.Add(opResult);
                CrmSyncEventSource.Log.UserSync(this._lastSyncDate, "RecoverDirtyUsers", result);

                opResult = SyncContacsForReactivatedAccounts();
                CrmSyncEventSource.Log.UserSync(this._lastSyncDate, "SyncContacsForReactivatedAccounts", opResult);

                opResult = SyncModifiedContacts(_crmGateway.ListChangedContacts(this._lastSyncDate));
                result.Add(opResult);
                CrmSyncEventSource.Log.UserSync(this._lastSyncDate, "SyncModifiedContacts", opResult);

                if (this._lastSyncDate > new DateTime(1901, 1, 1))
                {
                    opResult = RefreshUserWithOutofdateAccounts();
                    result.Add(opResult);
                    CrmSyncEventSource.Log.UserSync(this._lastSyncDate, "RefreshUserWithOutofdateAccounts", opResult);
                }

                ProcessInvalidUsers();
                
                this._accountsRepository.SaveLastSync(new Entities.Synchronization { Id = SyncType.User, LastSuccessfulSync = this._currentSyncDate });
                
                CrmSyncEventSource.Log.UsersSyncSucceeded(_currentSyncDate, result.Created, result.Deleted, result.Updated);
            }
            catch (Exception e)
            {
                CrmSyncEventSource.Log.UsersSyncFailed(_currentSyncDate, e.GetType().ToString(), e.Message, ExceptionUtility.GetInfo(e));
                throw new SyncFailedException("failed to sync the users", e);
            }
            finally
            {
                _cachedAccounts.Clear();
                _crmNullEmailContacts.Clear();
                _crmDuplicatedEmailContacts.Clear();
                _createdUsers.Clear();
                _updatedUsers.Clear();
                _invalidDuplicatedUsersIds.Clear();
                _invalidNullEmailUsersIds.Clear();

                CrmSyncEventSource.Log.UsersSyncStop(_currentSyncDate);
            }
        }

        private void ProcessInvalidUsers()
        {
            if (_crmDuplicatedEmailContacts.Count > 0 || _crmNullEmailContacts.Count > 0)
            {
                var newDuplicateEmailUsers = ProcessInvalidUsersByType(InvalidSynUserType.Duplicate, _crmDuplicatedEmailContacts);

                var newNullEmailUsers = ProcessInvalidUsersByType(InvalidSynUserType.NullEmail, _crmNullEmailContacts);

                if (newNullEmailUsers.Count > 0 || newDuplicateEmailUsers.Count > 0)
                {
                    SendEmail(newDuplicateEmailUsers, newNullEmailUsers);
                    
                    _accountsRepository.SaveInvalidSyncUsers(newNullEmailUsers);
                    newNullEmailUsers.ForEach(u => CrmSyncEventSource.Log.UserNullEmail(_currentSyncDate, u.Id, u.FirstName, u.MiddleName, u.LastName, u.AccountId.ToString(), u.Sync_AccountStatus.ToString()));
                    
                    _accountsRepository.SaveInvalidSyncUsers(newDuplicateEmailUsers);
                    newDuplicateEmailUsers.ForEach(u => CrmSyncEventSource.Log.UserDuplicateEmail(_currentSyncDate, u.Id, u.Email, u.FirstName, u.LastName, u.AccountId.ToString(), u.Sync_AccountStatus.ToString()));
                }
            }

            //get fixed users
            var crmFixedUserIds = _createdUsers.Union(_updatedUsers).Intersect<Guid>(_invalidDuplicatedUsersIds.Union(_invalidNullEmailUsersIds)).ToList();
            //delete fixed users from mongo
            _accountsRepository.DeleteInvalidSyncUsers(crmFixedUserIds);
        }

        private void SendEmail(List<User> newDuplicateEmailUsers, List<User> newNullEmailUsers)
        {
            //send email with newDuplicateEmailUsers + newNullEmailUsers
            var emailManager = new EmailManager(_invalidUsersEmailSubjectFileName, _invalidUsersEmailBodyFileName);
            emailManager.AddPlaceholderValue(_nullEmailListPlaceholder, ComposeEmailMessage(newNullEmailUsers));
            emailManager.AddPlaceholderValue(_duplicateEmailListPlaceholder, ComposeEmailMessage(newDuplicateEmailUsers));
            emailManager.SendEmail();
        }

        private String ComposeEmailMessage(List<User> invalidUsers)
        {
            var invalidUsersList = new StringBuilder();
            foreach (var user in invalidUsers)
            {
                invalidUsersList.AppendLine("<tr>");

                // Add first name column
                invalidUsersList.AppendLine(string.Format("<td style=\"font: normal 12px/15px Arial, sans-serif; color: #5f6061; border-bottom: 1px solid #cccccc;\">{0}</td>", user.FirstName));
                invalidUsersList.AppendLine("<td width=\"0\" style=\"border-bottom: 1px solid #cccccc;\">&nbsp;</td>");

                // Add middle name column
                invalidUsersList.AppendLine(string.Format("<td style=\"font: normal 12px/15px Arial, sans-serif; color: #5f6061; border-bottom: 1px solid #cccccc;\">{0}</td>", user.MiddleName));
                invalidUsersList.AppendLine("<td width=\"0\" style=\"border-bottom: 1px solid #cccccc;\">&nbsp;</td>");

                // Add last name column
                invalidUsersList.AppendLine(string.Format("<td style=\"font: normal 12px/15px Verdana, Arial, sans-serif; color: #5f6061; border-bottom: 1px solid #cccccc;\">{0}</td>", user.LastName));
                invalidUsersList.AppendLine("<td width=\"0\" style=\"border-bottom: 1px solid #cccccc;\">&nbsp;</td>");

                // Add nature of change column
                invalidUsersList.AppendLine(string.Format("<td style=\"font: normal 12px/15px Arial, sans-serif; color: #5f6061; border-bottom: 1px solid #cccccc;\">{0}</td>", user.Email));
                invalidUsersList.AppendLine("<td width=\"0\" style=\"border-bottom: 1px solid #cccccc;\">&nbsp;</td>");

                //End table row
                invalidUsersList.AppendLine("</tr>");

                // Add spacing between lines
                invalidUsersList.AppendLine("<tr><td style=\"line-height: 5px;\">&nbsp;</td></tr>");
            }
            return invalidUsersList.ToString();
        }

        private List<User> ProcessInvalidUsersByType(InvalidSynUserType type, List<CrmContact> crmInvalidContacts)
        {
            //read invalid users from Mongo:
            var oldInvalidUserIds = type == InvalidSynUserType.Duplicate ? _invalidDuplicatedUsersIds : _invalidNullEmailUsersIds;

            var crmInvalidUserIds = crmInvalidContacts.Select(c => c.Id);
            //get only new invalid users
            var newInvalidUserIds = crmInvalidUserIds.Except<Guid>(oldInvalidUserIds).ToList();
            //return a list of invalid users from the invalid crm contacts:
            return crmInvalidContacts.Where(u => newInvalidUserIds.Contains(u.Id)).Select(c => CreateNewUser(c)).ToList();
        }

        private SyncResult SyncContacsForReactivatedAccounts()
        {
            int pageSize = 30;
            int currentPage = 0;
            var result = new SyncResult();
            while (true)
            {

                var reactivatedAccountEvents = this._accountsRepository.GetReactivatedSyncEvents(currentPage * pageSize, pageSize);
                if (reactivatedAccountEvents == null || reactivatedAccountEvents.Count == 0)
                    return result;
                var opResult = SyncContactsForReactiveAccounts(reactivatedAccountEvents);
                result.Add(opResult);

                ++currentPage;
            }
        }

        private SyncResult SyncContactsForReactiveAccounts(List<ReactivatedSyncEvent> reactivatedAccountEvents)
        {
            var result = new SyncResult();
            foreach (var accountEvent in reactivatedAccountEvents)
            {
                var opResult = this.SyncModifiedContacts(this._crmGateway.ListContactsByAccountId(accountEvent.Id));
                result.Add(opResult);
                this._accountsRepository.DeleteReactivatedSyncEvent(accountEvent.Id);
            }
            return result;
        }

        private SyncResult RefreshUserWithOutofdateAccounts()
        {
            if (_lastSyncDate <= DateTime.MinValue)
                return new SyncResult();
            var accounts = this._accountsRepository.GetAccountsByModifiedAfter(this._lastSyncDate);
            if (accounts == null || accounts.Count == 0)
            {
                return new SyncResult();
            }

            int updatedNo = 0;
            foreach (var accountId in accounts) 
            { 
                var users = this._accountsRepository.GetUsersByAccountId(accountId, true);
                if(users != null)
                foreach(var user in users)
                {
                    this.RecomputeAndSaveUser(user, false);
                    updatedNo++;
                }
            }

            return new SyncResult { Updated = updatedNo };

        }

        private SyncResult RecoverDirtyUsers()
        {
            var dirtyUsers = this._accountsRepository.GetDirtyUsers(true);
            if (dirtyUsers == null || dirtyUsers.Count == 0)
            {
                return new SyncResult();
            }

            foreach (var user in dirtyUsers)
            {
                RecomputeAndSaveUser(user, true);
            }

            return new SyncResult { Updated = dirtyUsers.Count };
        }

        private void EnsureClientUser(User user)
        {
            ClientUser userclient = this._accountsRepository.TryGetClientUserByUserId(user.Id);
            if (userclient == null)
            {
                userclient = new ClientUser { Id = user.Id, PasswordExpirationDate = DateTime.UtcNow.AddYears(-1) };
                _accountsRepository.SaveClientUsers(new[] { userclient }.ToList());
            }
        }

        private void UpdateAccountStatus(User user)
        {
            if (user.AccountId == null)
            {
                user.Sync_AccountStatus = Status.Inactive;
                user.Account = null;
                return;
            }

            Account account = GetAccount(user.AccountId.Value);

            if (account == null)
            {
                user.Account = null;
                user.Sync_AccountStatus = Status.Unknown;
            }
            else
            {
                user.Account = account.Name;
                user.Sync_AccountStatus = account.HasPlatformAccess ? account.Status : Status.Inactive;
            }
        }

        private int SyncDeletedContacts()
        {
            var contactsIds = _crmGateway.ListContactsIds();
            var usersIds = _accountsRepository.ListUsersIds(true);
            if (usersIds == null || usersIds.Count == 0)
                return 0;

            var usersToDelete = usersIds.Except<Guid>(contactsIds).ToList();

            var deletedUsersCount = _accountsRepository.DeleteUsers(usersToDelete);

            _invalidDuplicatedUsersIds = _accountsRepository.GetInvalidSyncUsers(InvalidSynUserType.Duplicate);
            _invalidNullEmailUsersIds = _accountsRepository.GetInvalidSyncUsers(InvalidSynUserType.NullEmail);
            var invalidUsersIds = _invalidDuplicatedUsersIds.Union(_invalidNullEmailUsersIds);

            var invalidUsersToDelete = invalidUsersIds.Except<Guid>(contactsIds).ToList();

            _accountsRepository.DeleteInvalidSyncUsers(invalidUsersToDelete);

            return deletedUsersCount;
        }

        private SyncResult SyncModifiedContacts(List<CrmContact> contacts)
        {
            if (contacts == null || contacts.Count == 0)
            {
                return new SyncResult();
            }
            var toCreateContacts = new List<CrmContact>();

            foreach (var contact in contacts)
            {
                CrmSyncEventSource.Log.StartProcessingContact(this._currentSyncDate, contact.Id, contact.Email, contact.FirstName, contact.LastName);
                if (contact.Email == null)//ignore null email users
                {
                    _crmNullEmailContacts.Add(contact);
                    continue;
                }

                var user = _accountsRepository.TryGetUserByUserId(contact.Id);
                if (user == null)//new user
                {
                    if (contact.IsActive && contact.IsPlatformUser) //add only active accounts && platformUsers.
                    {
                        toCreateContacts.Add(contact);
                    }
                    continue;
                }

                if (IsDuplicated(contact, user))
                {
                    _crmDuplicatedEmailContacts.Add(contact);
                    continue;
                }
                else
                {
                    UpdateUser(contact, user);
                }
            }

            foreach (var contact in toCreateContacts)
            {
                if (_accountsRepository.TryGetUserByUsername(contact.Email) != null)
                    _crmDuplicatedEmailContacts.Add(contact);
                else
                {
                    CreateUser(contact);
                }
            }

            return new SyncResult { Created = _createdUsers.Count, Updated = _updatedUsers.Count };
        }

        private bool IsDuplicated(CrmContact contact, User user)
        {
            if (contact.Email.ToLowerInvariant() == user.Email.ToLowerInvariant())//email haven't changed
                return false;

            return _accountsRepository.TryGetUserByUsername(contact.Email) != null;
        }

        private void CreateUser(CrmContact contact)
        {
            var user = CreateNewUser(contact);
            _createdUsers.Add(user.Id);
            RecomputeAndSaveUser(user, true);
        }

         private User CreateNewUser(CrmContact contact)
         {
             var user = new User();

             user.Id = contact.Id;
             user.Sync_DiscretStatus = Status.Unknown;

             CopyToUser(contact, user);
             return user;
         }

        private void UpdateUser(CrmContact contact, User user)
        {
            if (contact == null || user == null)
            {
                return;
            }
            _updatedUsers.Add(user.Id);
            CopyToUser(contact, user);
            user.Sync_IsDeleted = false;
            RecomputeAndSaveUser(user, false);
        }
        private void RecomputeAndSaveUser(User user, bool createClientUser)
        {
            if (!user.IsFakeIpUser)//never touch the FakeIpUser!!!!!!!!!!!!!
            {
                UpdateAccountStatus(user);

                bool requireActivation = ComputeUserStatus(user, false);

                user.Sync_SyncState = user.Sync_AccountStatus == Status.Unknown ? SyncUserState.PartialyInSync : SyncUserState.Syncronized;

                if (!requireActivation)
                {
                    ComputeUserStatus(user, true);
                }

                user.Sync_IsDirty = createClientUser || requireActivation || (user.Sync_SyncState != SyncUserState.Syncronized);
                user.Status = (!user.Sync_IsDirty) && user.Status == Status.Active ? Status.Active : Status.Inactive;

                user.SyncOn = this._currentSyncDate;

                //save the progress
                this._accountsRepository.SaveUser(user);

                if (createClientUser)
                    EnsureClientUser(user);

                if (user.Sync_SyncState != SyncUserState.Syncronized)
                {
                    return; // the user remain dirty , retry later
                }

                if (ComputeUserStatus(user, true))
                {
                    CompleteActivation(user);
                }

                user.Sync_IsDirty = false;
                this._accountsRepository.SaveUser(user);
                CrmSyncEventSource.Log.UserSyncSave(this._currentSyncDate, user);
            }
        }

        private static void CopyToUser(CrmContact contact, User user)
        {
            user.AccountId = contact.AccountId;
            user.Email = contact.Email;
            user.FirstName = contact.FirstName;
            user.FullName = string.IsNullOrEmpty(contact.FullName) ? contact.FirstName + " " + contact.LastName : contact.FullName;
            user.MiddleName = contact.MiddleName;
            
            user.LastName = contact.LastName;
            user.ModifiedOn = contact.ModifiedOn;
            user.Sync_IsPlatformUser = contact.IsPlatformUser;
            user.Sync_IsActive = contact.IsActive;
        }

        private void CompleteActivation(User user)
        {
            if (user.Email != null)
            {
                Guard.ArgumentNotNull(user, "user");

                ClientUser clientUser = new ClientUser();
                clientUser.Id = user.Id;
                clientUser.PasswordExpirationDate = DateTime.UtcNow.AddYears(-1);

                this._accountsRepository.SaveClientUsers(new[] { clientUser }.ToList());
                
                this._accountsRepository.SaveUser(user);
                this._authOperation.ResetPasswordByUser(user, PasswordEmailMessageBody.AccessGranted);

                CrmSyncEventSource.Log.ResetPasswordEmail(this._currentSyncDate, user.Id, user.Email);
            }
        }

        private bool ComputeUserStatus(User user, bool updateUser)
        {
            if (user == null)
                return false;
            
            bool isActive = !user.Sync_IsDeleted && user.Sync_AccountStatus == Status.Active && user.Sync_IsPlatformUser && user.Sync_IsActive;
            var previousStatus = user.Sync_DiscretStatus;

            if (updateUser)
            {
                user.Status = isActive ? Status.Active : Status.Inactive;

                if (user.Sync_AccountStatus != Status.Unknown)
                {
                    user.Sync_DiscretStatus = user.Status;
                }
            }

            return isActive && previousStatus != Status.Active;
        }

        private Account GetAccount(Guid guid)
        {
            Account account;
            if (this._cachedAccounts.TryGetValue(guid, out account))
            {
                return account;
            }

            account =  this._accountsRepository.TryGetAccountById(guid);
            
            if (account != null) 
            {
                this._cachedAccounts[account.Id] = account;
            }
            
            return account;
        }

        public struct SyncResult
        {
            public int Created;
            public int Deleted;
            public int Updated;

            public void Add(SyncResult opResult)
            {
                Created += opResult.Created;
                Deleted += opResult.Deleted;
                Updated += opResult.Updated;
            }
        }
    }
}
